/*******************************************************************************
 * Copyright 2011 Mauro Luigi Drago (drago.luigi@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/
package com.bytenose.extrautils.collect;

import java.util.Iterator;
import java.util.NoSuchElementException;

import com.bytenose.extrautils.annotations.NonNullable;
import com.bytenose.extrautils.annotations.UtilityClass;
import com.bytenose.extrautils.functional.*;

/**
 * Defines useful methods for iterables.
 * @author Mauro Luigi Drago
 *
 */
@UtilityClass
public final class Iterables2 {
	
	/**
	 * Private constructor to avoid instantiation.
	 */
	private Iterables2() {
	}
	
	/**
	 * Iterates stepwise over a set of {@link Iterable}s and applies a specified method 
	 * to each element tuple.
	 * The iterables must yield the same number of elements, 
	 * otherwise an exception is raised.
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param it1 the first iterable, cannot be null.
	 * @param it2 the second iterable, cannot be null.
	 * @param method the method to apply, cannot be null.
	 * 
	 * @throws NoSuchElementException 
	 * 		if iterables do not yield the same number of elements.
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	public static <T1, T2> void applyWise(
			@NonNullable Iterable<T1> it1, @NonNullable Iterable<T2> it2, 
			@NonNullable Method2<T1,T2> method)
			throws IllegalArgumentException, NoSuchElementException {
		WiseIterators.applyWise(it1.iterator(), it2.iterator(), method);
	}
	
	/**
	 * Iterates stepwise over a set of {@link Iterable}s and applies a specified method 
	 * to each element tuple.
	 * The iterables must yield the same number of elements, 
	 * otherwise an exception is raised.
	 * @param <T1> the type of the first argument
	 * @param <T2> the type of the second argument.
	 * @param <T3> the type of the third argument.
	 * @param it1 the first iterable, cannot be null.
	 * @param it2 the second iterable, cannot be null.
	 * @param it3 the third iterable, cannot be null.
	 * @param method the method to apply, cannot be null.
	 * 
	 * @throws NoSuchElementException 
	 * 		if iterables do not yield the same number of elements.
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	public static <T1, T2, T3> void applyWise(
			@NonNullable Iterable<T1> it1, @NonNullable Iterable<T2> it2, 
			@NonNullable Iterable<T3> it3,
			@NonNullable Method3<T1,T2,T3> method)
			throws IllegalArgumentException, NoSuchElementException {
		WiseIterators.applyWise(it1.iterator(), it2.iterator(), it3.iterator(), method);
	}
	
	/**
	 * Iterates stepwise over a set of {@link Iterable}s and applies a specified method 
	 * to each element tuple.
	 * The iterables must yield the same number of elements, 
	 * otherwise an exception is raised.
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param <T3> the type of the third argument.
	 * @param <T4> the type of the fourth argument.
	 * @param it1 the first iterable, cannot be null.
	 * @param it2 the second iterable, cannot be null.
	 * @param it3 the third iterable, cannot be null.
	 * @param it4 the fourth iterable, cannot be null.
	 * @param method the method to apply, cannot be null.
	 * 
	 * @throws NoSuchElementException 
	 * 		if iterables do not yield the same number of elements.
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	public static <T1, T2, T3, T4> void applyWise(
			@NonNullable Iterable<T1> it1, @NonNullable Iterable<T2> it2, 
			@NonNullable Iterable<T3> it3, @NonNullable Iterable<T4> it4, 
			@NonNullable Method4<T1,T2,T3, T4> method)
			throws IllegalArgumentException, NoSuchElementException {
		WiseIterators.applyWise(it1.iterator(), it2.iterator(), it3.iterator(), 
				it4.iterator(), method);
	}
	
	/**
	 * Iterates stepwise over a set of {@link Iterable}s and applies a specified method 
	 * to each element tuple.
	 * The iterables may yield a different number of values. If this is the case, i.e., some
	 * elements miss, the method is invoked with a null argument for the corresponding
	 * parameter.
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param it1 the first iterable, cannot be null.
	 * @param it2 the second iterable, cannot be null.
	 * @param method the method to apply, cannot be null.
	 * 
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	public static <T1, T2> void applyWiseWeak(
			@NonNullable Iterable<T1> it1, @NonNullable Iterable<T2> it2, 
			@NonNullable Method2<T1,T2> method) 
			throws IllegalArgumentException {
		WeakWiseIterators.applyWiseWeak(it1.iterator(), it2.iterator(), method);
	}
	
	/**
	 * Iterates stepwise over a set of {@link Iterable}s and applies a specified method 
	 * to each element tuple.
	 * The iterables may yield a different number of values. If this is the case, i.e., some
	 * elements miss, the method is invoked with a null argument for the corresponding
	 * parameter.
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param <T3> the type of the third argument.
	 * @param it1 the first iterable, cannot be null.
	 * @param it2 the second iterable, cannot be null.
	 * @param it3 the third iterable, cannot be null.
	 * @param method the method to apply, cannot be null.
	 * 
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	public static <T1, T2, T3> void applyWiseWeak(
			@NonNullable Iterable<T1> it1, @NonNullable Iterable<T2> it2, 
			@NonNullable Iterable<T3> it3,
			@NonNullable Method3<T1,T2,T3> method)
			throws IllegalArgumentException {
		WeakWiseIterators.applyWiseWeak(it1.iterator(), it2.iterator(), 
				it3.iterator(), method);
	}
	
	/**
	 * Iterates stepwise over a set of {@link Iterable}s and applies a specified method 
	 * to each element tuple.
	 * The iterables may yield a different number of values. If this is the case, i.e., some
	 * elements miss, the method is invoked with a null argument for the corresponding
	 * parameter.
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param <T3> the type of the third argument.
	 * @param <T4> the type of the fourth argument.
	 * @param it1 the first iterable, cannot be null.
	 * @param it2 the second iterable, cannot be null.
	 * @param it3 the third iterable, cannot be null.
	 * @param it4 the fourth iterable, cannot be null.
	 * @param method the method to apply, cannot be null.
	 * 
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	public static <T1, T2, T3, T4> void applyWiseWeak(
			@NonNullable Iterable<T1> it1, @NonNullable Iterable<T2> it2, 
			@NonNullable Iterable<T3> it3, @NonNullable Iterable<T4> it4,
			@NonNullable Method4<T1,T2,T3,T4> method)
			throws IllegalArgumentException {
		WeakWiseIterators.applyWiseWeak(it1.iterator(), it2.iterator(), 
				it3.iterator(), it4.iterator(), method);
	}
	
	/**
	 * Iterates stepwise over a set of {@link Iterable}s and zips elements 
	 * into tuples.
	 * The iterables must yield the same number of elements, 
	 * otherwise an exception is raised.
	 * The returned iterable is a non-materialized iterable yielding mutable tuples.
	 * To improve performance, always the same tuple object is returned
	 * over subsequent invocations of the {@link Iterator#next()} with
	 * tuple values reflecting the currently scanned elements.
	 * This avoid creating a lot of unnecessary objects, but limits the 
	 * applicability of the method.
	 * If values must be cached across different invocations of the 
	 * {@link Iterator#next()} method, first materialize the iterable
	 * or the tuple.
	 * The returned iterable and its associated iterator support {@link Iterator#remove()} 
	 * if the iterables specified as the method arguments support that functionality.
	 * 
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param it1 the first iterable, cannot be null.
	 * @param it2 the second iterable, cannot be null.
	 * 
	 * @return the iterable of the zipped elements.
	 * 
	 * @throws NoSuchElementException 
	 * 		if iterables do not yield the same number of elements.
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	public static <T1,T2> Iterable<MutableTuple2<T1,T2>> zip(
			@NonNullable final Iterable<T1> it1, 
			@NonNullable final Iterable<T2> it2) 
			throws IllegalArgumentException, NoSuchElementException {
		return new Iterable<MutableTuple2<T1,T2>>() {
			@Override
			public Iterator<MutableTuple2<T1, T2>> iterator() {
				return ZipIterators.zip(it1.iterator(), it2.iterator());
			}
		};
	}
	
	/**
	 * Iterates stepwise over a set of {@link Iterable}s and zips elements 
	 * into tuples.
	 * The iterables must yield the same number of elements, 
	 * otherwise an exception is raised.
	 * The returned iterable is a non-materialized iterator yielding mutable tuples.
	 * To improve performance, always the same tuple object is returned
	 * over subsequent invocations of the {@link Iterator#next()} with
	 * tuple values reflecting the currently scanned elements.
	 * This avoid creating a lot of unnecessary objects, but limits the 
	 * applicability of the method.
	 * If values must be cached across different invocations of the 
	 * {@link Iterator#next()} method, first materialize the iterable
	 * or the tuple.
	 * The returned iterable and its associated iterator support {@link Iterator#remove()} 
	 * if the iterables specified as the method arguments support that functionality.
	 * 
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param <T3> the type of the third argument.
	 * @param it1 the first iterable, cannot be null.
	 * @param it2 the second iterable, cannot be null.
	 * @param it3 the third iterable, cannot be null.
	 * 
	 * @return the iterable of the zipped elements.
	 * 
	 * @throws NoSuchElementException 
	 * 		if iterators do not yield the same number of elements.
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	public static <T1, T2, T3> Iterable<MutableTuple3<T1,T2,T3>> zip(
			@NonNullable final Iterable<T1> it1, 
			@NonNullable final Iterable<T2> it2,
			@NonNullable final Iterable<T3> it3) 
			throws IllegalArgumentException, NoSuchElementException {
		return new Iterable<MutableTuple3<T1,T2,T3>>() {
			@Override
			public Iterator<MutableTuple3<T1, T2, T3>> iterator() {
				return ZipIterators.zip(it1.iterator(), it2.iterator(), it3.iterator());
			}
		};
	}
	
	/**
	 * Iterates stepwise over a set of {@link Iterable}s and zips elements 
	 * into tuples.
	 * The iterables must yield the same number of elements, 
	 * otherwise an exception is raised.
	 * The returned iterable is a non-materialized iterator yielding mutable tuples.
	 * To improve performance, always the same tuple object is return
	 * over subsequent invocations of the {@link Iterator#next()} with
	 * tuple values reflecting the currently scanned elements.
	 * This avoid creating a lot of unnecessary objects, but limits the 
	 * applicability of the method.
	 * If values must be cached across different invocations of the 
	 * {@link Iterator#next()} method, first materialize the iterable
	 * or the tuple.
	 * The returned iterable and its associated iterator support {@link Iterator#remove()} 
	 * if the iterables specified as the method arguments support that functionality.
	 * 
	 * @return the iterable of the zipped elements.
	 * 
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param <T3> the type of the third argument.
	 * @param <T4> the type of the fourth argument.
	 * @param it1 the first iterable, cannot be null.
	 * @param it2 the second iterable, cannot be null.
	 * @param it3 the third iterable, cannot be null.
	 * @param it4 the fourth iterable, cannot be null.
	 * 
	 * @throws NoSuchElementException 
	 * 		if iterables do not yield the same number of elements.
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	public static <T1,T2,T3,T4> Iterable<MutableTuple4<T1,T2,T3,T4>> zip(
			@NonNullable final Iterable<T1> it1, 
			@NonNullable final Iterable<T2> it2,
			@NonNullable final Iterable<T3> it3,
			@NonNullable final Iterable<T4> it4) 
			throws IllegalArgumentException, NoSuchElementException {
		return new Iterable<MutableTuple4<T1,T2,T3,T4>>() {
			@Override
			public Iterator<MutableTuple4<T1, T2, T3, T4>> iterator() {
				return ZipIterators.zip(it1.iterator(), it2.iterator(), 
						it3.iterator(), it4.iterator());
			}
		};
	}
	
	/**
	 * Iterates stepwise over a set of {@link Iterable}s and zips elements 
	 * into tuples.
	 * The iterables may yield a different number of elements. 
	 * If this is the case, i.e., some elements miss, 
	 * the tuples yielded by the returned iterables have null values for the 
	 * missing elements. 
	 * The returned iterable is a non-materialized iterator yielding mutable tuples.
	 * To improve performance, always the same tuple object is return
	 * over subsequent invocations of the {@link Iterator#next()} with
	 * tuple values reflecting the currently scanned elements.
	 * This avoid creating a lot of unnecessary objects, but limits the 
	 * applicability of the method.
	 * If values must be cached across different invocations of the 
	 * {@link Iterator#next()} method, first materialize the iterable
	 * or the tuple.
	 * The returned iterable and its associated iterator support {@link Iterator#remove()} 
	 * if the iterables specified as the method arguments support that functionality.
	 * 
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param it1 the first iterable, cannot be null.
	 * @param it2 the second iterable, cannot be null.
	 * 
	 * @return the iterable of the zipped elements.
	 * 
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	public static <T1,T2> Iterable<MutableTuple2<T1,T2>> zipWeak(
			@NonNullable final Iterable<T1> it1, 
			@NonNullable final Iterable<T2> it2) 
			throws IllegalArgumentException {
		return new Iterable<MutableTuple2<T1,T2>>() {
			@Override
			public Iterator<MutableTuple2<T1, T2>> iterator() {
				return WeakZipIterators.zipWeak(it1.iterator(), it2.iterator());
			}
		};
	}
	
	/**
	 * Iterates stepwise over a set of {@link Iterable}s and zips elements 
	 * into tuples.
	 * The iterables may yield a different number of elements. 
	 * If this is the case, i.e., some elements miss, 
	 * the tuples yielded by the returned iterable have null values for the 
	 * missing elements.
	 * The returned iterable is a non-materialized iterator yielding mutable tuples.
	 * To improve performance, always the same tuple object is return
	 * over subsequent invocations of the {@link Iterator#next()} with
	 * tuple values reflecting the currently scanned elements.
	 * This avoid creating a lot of unnecessary objects, but limits the 
	 * applicability of the method.
	 * If values must be cached across different invocations of the 
	 * {@link Iterator#next()} method, first materialize the iterable
	 * or the tuple.
	 * The returned iterable and its associated iterator support {@link Iterator#remove()} 
	 * if the iterables specified as the method arguments support that functionality.
	 * 
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param <T3> the type of the third argument.
	 * @param it1 the first iterable, cannot be null.
	 * @param it2 the second iterable, cannot be null.
	 * @param it3 the third iterable, cannot be null.
	 * 
	 * @return the iterable of the zipped elements.
	 * 
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	public static <T1, T2, T3> Iterable<MutableTuple3<T1,T2,T3>> zipWeak(
			@NonNullable final Iterable<T1> it1, 
			@NonNullable final Iterable<T2> it2,
			@NonNullable final Iterable<T3> it3) 
			throws IllegalArgumentException {
		return new Iterable<MutableTuple3<T1,T2,T3>>() {
			@Override
			public Iterator<MutableTuple3<T1, T2, T3>> iterator() {
				return WeakZipIterators.zipWeak(it1.iterator(), it2.iterator(), 
						it3.iterator());
			}
		};	
	}
	
	/**
	 * Iterates stepwise over a set of {@link Iterable}s and zips elements 
	 * into tuples.
	 * The iterables may yield a different number of elements. 
	 * If this is the case, i.e., some elements miss, 
	 * the tuples yielded by the returned iterable have null values for the 
	 * missing elements.
	 * The returned iterable is a non-materialized iterator yielding mutable tuples.
	 * To improve performance, always the same tuple object is return
	 * over subsequent invocations of the {@link Iterator#next()} with
	 * tuple values reflecting the currently scanned elements.
	 * This avoid creating a lot of unnecessary objects, but limits the 
	 * applicability of the iterator.
	 * If values must be cached across different invocations of the 
	 * {@link Iterator#next()} method, first materialize the iterable
	 * or the tuple.
	 * The returned iterable and its associated iterator support {@link Iterator#remove()} 
	 * if the iterables specified as the method arguments support that functionality.
	 * 
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param <T3> the type of the third argument.
	 * @param <T4> the type of the fourth argument.
	 * @param it1 the first iterable, cannot be null.
	 * @param it2 the second iterable, cannot be null.
	 * @param it3 the third iterable, cannot be null.
	 * @param it4 the fourth iterable, cannot be null.
	 * 
	 * @return the iterable of the zipped elements.
	 * 
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	public static <T1,T2,T3,T4> Iterable<MutableTuple4<T1,T2,T3,T4>> zipWeak(
			@NonNullable final Iterable<T1> it1, 
			@NonNullable final Iterable<T2> it2,
			@NonNullable final Iterable<T3> it3,
			@NonNullable final Iterable<T4> it4) 
			throws IllegalArgumentException {
		return new Iterable<MutableTuple4<T1,T2,T3,T4>>() {
			@Override
			public Iterator<MutableTuple4<T1, T2, T3, T4>> iterator() {
				return WeakZipIterators.zipWeak(it1.iterator(), it2.iterator(), 
						it3.iterator(), it4.iterator());
			}
		};
	}
	
	/**
	 * Materializes a non-materialized tuples {@link Iterable}.
	 * The returned iterable is a non-materialized iterable yielding mutable tuples.
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param tuplesIterable the tuples iterable, cannot be null.
	 * 
	 * @throws NoSuchElementException 
	 * 		if iterables do not yield the same number of elements.
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	public static <T1,T2> Iterable<ITuple2<T1,T2>> materialize2(
			@NonNullable final Iterable<? extends ITuple2<T1,T2>> tuplesIterable) 
			throws IllegalArgumentException, NoSuchElementException {
		return new Iterable<ITuple2<T1,T2>>() {
			@Override
			public Iterator<ITuple2<T1, T2>> iterator() {
				return Materialization.materialize2(tuplesIterable.iterator());
			}
		};
	}
	
	/**
	 * Materializes a non-materialized tuples {@link Iterable}.
	 * The returned iterable is a non-materialized iterable yielding mutable tuples.
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param <T3> the type of the third argument.
	 * @param tuplesIterable the tuples iterable, cannot be null.
	 * 
	 * @throws NoSuchElementException 
	 * 		if iterators do not yield the same number of elements.
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	public static <T1, T2, T3> Iterable<ITuple3<T1,T2,T3>> materialize3(
			@NonNullable final Iterable<? extends ITuple3<T1,T2,T3>> tuplesIterable) 
			throws IllegalArgumentException, NoSuchElementException {
		return new Iterable<ITuple3<T1,T2,T3>>() {
			@Override
			public Iterator<ITuple3<T1, T2, T3>> iterator() {
				return Materialization.materialize3(tuplesIterable.iterator());
			}
		};
	}
	
	/**
	 * Materializes a non-materialized tuples {@link Iterable}.
	 * The returned iterable is a non-materialized iterable yielding mutable tuples.
	 * @param <T1> the type of the first argument.
	 * @param <T2> the type of the second argument.
	 * @param <T3> the type of the third argument.
	 * @param <T4> the type of the fourth argument.
	 * @param tuplesIterable the tuples iterable, cannot be null.
	 * 
	 * @throws NoSuchElementException 
	 * 		if iterators do not yield the same number of elements.
	 * @throws IllegalArgumentException 
	 * 		if some of the {@link NonNullable} arguments are null.
	 */
	@NonNullable
	public static <T1,T2,T3,T4> Iterable<ITuple4<T1,T2,T3,T4>> materialize4(
			@NonNullable final Iterable<? extends ITuple4<T1,T2,T3,T4>> tuplesIterable) 
			throws IllegalArgumentException, NoSuchElementException {
		return new Iterable<ITuple4<T1,T2,T3,T4>>() {
			@Override
			public Iterator<ITuple4<T1, T2, T3, T4>> iterator() {
				return Materialization.materialize4(tuplesIterable.iterator());
			}
		};
	}
}
